#!/usr/bin/python
# -*- coding: utf-8 -*-

u"""
Here are time related classes.
"""

__version__ = '$Id$'


import warnings
from bisect import insort
from time import time
from sys import maxint

from events import Event

#-------------------------------------------------------------------------------

class GameTime(object):
    u"""
    GameTime allows you to manipulate the virtual time in the game. You can
    slow down or speed up the time in your game. Also you can pause any time 
    dependent code by setting the dilatation factor to 0. Tying to set a 
    negative factor value will raise a NegativeFactorException.
    Each call to update will fire the (public) 'event_update' with the 
    arguments: gdt, gt, dt, t 
    (game delta time, game time, real delta time, real time)
    
    Through the attribute time the starttime can be set and the current
    gametime can be read (both in [ms]).
    
    GameTime provides also a scheduler part, in which functions can 
    be scheduled to be called later. The scheduler takes time dilatation
    into account. The scheduler part does not work if blocking mode is used 
    in gamebase (and the clock part isnt that usefull then). Use the Scheduler 
    class if a working scheduler even in blocking mode.
    """
    
    SCHEDULE_STOP = True
    SCHEDULE_AGAIN = False
    
    def __init__(self, *args, **kwargs):
        u"""
        Init the GameTime with tick_speed=1.0.

        :Variables:
            time : int
                game time in [ms] since start, can be read and written
            event_update : Event
                update event that is fired every time update is called, 
                it passes gdt, gt, dt t as parameters
        
        """
        super(GameTime, self).__init__(*args, **kwargs)
        # clock attributes
        self._factor = 1.0
        self.time = 0
        self.event_update = Event(u"gametime event_update") # update(gdt, gt, dt, t)
        # scheduling attributes
        self._tasks = [] # [(gt, (f, arg, kwargs)), ]
        self._unschedule_func = None

    def _set_factor(self, factor):
        u"""
        For use in the factor property only. You get a warning when setting a
        negative value, but it is permitted.
        
        :Parameters:
            factor : float
              time dilatation factor, 0.0 pauses, negative values run the time 
              backwards (scheduling does not work when running backwards).
        """
        if __debug__:
              if 0 > factor:
                warnings.warn(u'Using negative time factor!!')
        if 0 == factor:
            # TODO: raise exception
            self._factor = 0
        else:
            self._factor = factor

    tick_speed = property(lambda self: self._factor, _set_factor, doc=u'Set the \
dilatation factor, 0.0 pauses, negative raise a warning and let the time run \
backwards (scheduling does not work with negative values)')

    #-- scheduler methods --#

    def schedule(self, dt, func, *args, **kwargs):
        u"""
        Schedule a function with its arguments. It will be called once after 
        the time specified by dt [ms] and removed from scheduling.
        
        :Parameters:
            dt : float
                delta game time after which the func should be called in [ms]
            func : function ref
                function to when its time has come
            args : tuple
                arguments for the function call
            kwargs : dict
                keyword arguments of the function call
        """
        # schedule for later call
        t = self.time + dt
        # add task to the sorted list
        task = (t, (func, args, kwargs))
        insort(self._tasks, task)

    def _reschedule(self, *args, **kwargs):
        u"""
        Internal use only. Used for re-scheduling functions to implement 
        schedule_repeated.
        """
        interval, old_t, func, eargs = args
        # call callback
        self._unschedule_func = None
        stop = func(*eargs, **kwargs) or func == self._unschedule_func
        t = old_t + interval
        # time leaping, if current time is too far in the future, then call is 
        # as many times as the interval has elapsed
        while t < self.time and not stop:
            old_t = t
            t = old_t + interval
            stop = func(*eargs,  **kwargs) or func == self._unschedule_func
        # re-schedule the func, adapting dt so it is exact
        if not stop:
            self.schedule(t - self.time, self._reschedule, interval, t, func, eargs, **kwargs)

    def schedule_repeated(self, interval, func, *args, **kwargs):
        u"""
        The given function will be called repeatedly with the given time 
        interval [ms]. To stop the scheduling either call unschedule(func) or 
        just return SCHEDULE_STOP from the function func.
        
        :Parameters:
            interval : float
                game time between each call in [ms]
            func : func ref
                function that is called
            args : tuple
                functions arguments
            kwargs : dict
                keyword arguments for func
        """
        self.schedule(interval, self._reschedule, interval, self.time + interval, func, args, **kwargs)

    def unschedule(self, func):
        u"""
        Remove the function from any scheduling.
        
        :Parameters:
            func : function ref
                function to remove
        """
        self._unschedule_func = func
        # find and remove func from tasks (all occurences)
        self._tasks = [task for task in self._tasks if task[1][0] != func]
        # remove repeated tasks too
        remaining = []
        for task in self._tasks:
            if task[1][0] == self._reschedule:
                if task[1][1][2] != func:
                    remaining.append(task)
            else:
                remaining.append(task)
        self._tasks = remaining

    def clear(self):
        u"""
        Removes all tasks from the scheduler.
        """
        self._tasks = []

    def update(self, dt, real_t):
        u"""
        Should be called each frame to update the gametime. 
        dt is the time passes in 
        the last 'frame' and t should be the real time. It fires the 
        event_update with the arguments: gdt, gt, dt, t
        
            gdt : float
                game delta time in [ms]
            gt : float
                game time in [ms] since start
            dt : int
                real delta time (same as input) in [ms]
            real_t : int
                real time (same as input) in [ms]
        
        :Parameters:
            dt : int
                real delta time (same as input) in [ms]
            real_t : int
                real time (same as input) in [ms]
        
        The gametime is only advanced if update is called.
        
        It also calls all scheduled functions if their time has come. The 
        scheduled functions will be called in the correct order, but only
        with a time resolution equal to call to this update method (probably
        ~30 fps).
        """
        gdt = self._factor * dt
        self.time += gdt
        if self._factor:
            # call scheduled func if their time has come
            while self._tasks and self._tasks[0][0] <= self.time:
                t, fargs = self._tasks.pop(0)
                func, args, kwargs = fargs
                func(*args, **kwargs)
        # fire event
        self.event_update(gdt, self.time, dt, real_t)

    def timespan_real_to_game_time(self, real_time_span):
        if self._factor:
            return real_time_span / self._factor
        else:
            return maxint

    def timespan_game_time_to_real(self, game_time_span):
        return game_time_span * self._factor

#-------------------------------------------------------------------------------
class StopWatch(object):
    u"""
    Simple stopwatch. It uses the standard time.time() internally and therefor 
    it is not that precise (should be enough for most purposes).
    """

    _RUNNING, _STOPED = range(2)

    def __init__(self, run=False, *args, **kwargs):
        u"""
        If run is True, then it starts to run.
        """
        super(StopWatch, self).__init__(*args, **kwargs)
        self._start_time = 0
        self._stop_time = 0
        self._state = self._STOPED
        if run:
            self.start(True)

    def _get_elapsed(self):
        u"""
        Internal use only.
        
        :rtype: float [ms]
        """
        if self._RUNNING == self._state:
            return (time() - self._start_time) * 1000.0
        else:
            return (self._stop_time - self._start_time) * 1000.0

    is_running = property(lambda self: True if self._RUNNING == self._state \
                                            else False, doc=u'readonly, either True or False')
    elapsed = property(_get_elapsed, doc=u'returns the elapsed time since start in [ms] or 0 if it hasn\'t been started yet.')

    def start(self, reset = False):
        u"""
        Starts the stopwatch. Repeated calls do not have an effect, as long 
        reset is False. If reset is set to True, then the stopwatch is 
        restarted. Each time the stopwatch is started, the stop_time is set
        equal to the start_time while the stopwatch is running.
        """
        if reset or self._STOPED == self._state:
            self._state = self._RUNNING
            self._start_time = time()
            self._stop_time = self._start_time
                

    def stop(self):
        u"""
        Stops the stopwatch and returns the elapsed time since start in [ms].
        
        :rtype: float [ms]
        """
        if self._RUNNING == self._state:
            self._stop_time = time()
        self._state = self._STOPED
        return self.elapsed


#-------------------------------------------------------------------------------

class Scheduler(object):
    u"""
    High precision scheduler. It uses the timer given by the GameBase instance.
    The GameBase instance has to implement following functions in order to make
    this scheduler functional:
    
        - _get_time(self) -> returns current time
        - _schedule_once(self, dt, func)
        - _unschedule(self, func)
    
    And there only can be on Scheduler instance per GameState.
    
    This scheduler only works with real time. In return if also works in block 
    mode of the GameBase.
    """

    def __init__(self, *args, **kwargs):
        u"""
        Scheduler init.
        """
        super(Scheduler, self).__init__(*args, **kwargs)
        self._tasks = [] # [(t, (f, arg, kwargs)), ]
        self._schedule_once = None
        self._unschedule = None
        self._now = None
        self.TOL = 0
        self._pause_start = 0

    def schedule(self, dt, func, *args, **kwargs):
        u"""
        Add a function for scheduling, call it when dt has passed.
        """
        t = self._now() + dt
        task = (t, (func, args, kwargs))
        insort(self._tasks, task)
        # TODO: optimize?
        self._unschedule(self._update)
        if dt < self.TOL:
            self._update()
        else:
            self._schedule_once( self._tasks[0][0] - self._now(), self._update)

    def unschedule(self, func):
        u"""
        Remove function from scheduling (all occurences).
        """
        # stop scheduler
        self._unschedule(self._update)
        # find and remove func from tasks
        self._tasks = [task for task in self._tasks if task[1][0] != func]
        # schedule next task if remaining
        if self._tasks:
            self._schedule_once(self._tasks[0][0] - self._now(), self._update)

    def _update(self, *args, **kwargs):
        u"""
        Internal use only. Will be called from the GameBase timer.
        """
        # call callback(s)
        now = self._now() + self.TOL
        while self._tasks and self._tasks[0][0] <= now:
            t, fargs = self._tasks.pop(0)
            func, args, kwargs = fargs
            func(*args, **kwargs)
        # schedule next task
        if self._tasks:
            self._schedule_once(self._tasks[0][0] - self._now(), self._update)
        else:
            self._unschedule(self._update)

    def on_activate(self, game_base):
        u"""
        Reconnect to the GameBase timer when the state is activated.
        """
        self._schedule_once = game_base._schedule_once
        self._unschedule = game_base._unschedule
        self._now = game_base._get_time
        paused_time = self._now() - self._pause_start
        self._tasks = [(t + paused_time, cb) for t, cb in self._tasks]
        # schedule next task if remaining
        if self._tasks:
            self._schedule_once(self._tasks[0][0] - self._now(), self._update)

    def on_deactivate(self):
        u"""
        Disconnect from the GameBase timer when the state is deactivated.
        """
        self._pause_start = self._now()
        self._unschedule(self._update)
        self._schedule_once = None
        self._unschedule = None
        self._now = None



